The basic model of a programming language is a set of symbols specified according to a grammar which is parsed and translated into an executable form according to semantic rules of the language. In this form, a language is “literal” in the sense that each input construct references to a specific construct in the output representation. That is, a procedure specification produces an executable procedure specification and a struct or type or class specification produces a memory template (set of offsets) for accessing elements of the struct, type or class in memory.
Various ways exist to avoid repeated specification and to recognize common processing sequences and data structures. In particular a common subprocessing sequence that occurs in multiple locations can be replaced by calls to a common subroutine that performs that common processing sequence. A common subexpression occurring in multiple expressions can also be factored out as an expression that assigns to a local variable or as a separate function that returns the value of the subexpression. Similarly, common data structures can be specified in a common type/class or struct specification, with data members specified by this common type. Object-oriented languages allow commonality between different types to be recognized and specified using a base type from which these different types inherit, effectively sharing base type data member and member function definitions and implementation.
However, logical commonality can arise that includes differences that cannot be handled by these sharing mechanisms. In particular, two specifications of a data structure implementation can be the same except for the type of the object to be stored in this data structure. For example, two implementations of a hash table may differ only in the type of object that each can store. Similarly, a processing sequence may be the same for two different computing platforms, differing only in the name of a constant being passed to a call.
It is possible to handle some such cases by performing actions at run-time, including automatic run-time checks. For example, a constant can be changed to a variable with this variable having a value automatically set at run-time according to the target platform. Similarly, a hash table can have a base type (possibly the so-called “void” base type pointer) and perform run-time checks of the types actually stored in the table to ensure type safety. However, this approach incurs the risk of only discovering problems at run-time. It also incurs the cost of performing run-time checks for aspects that are actually known at translation time, and thus are logically unnecessary. Finally, for some cases such as Verilog generation and embedded software, it may not be feasible to perform run-time checks. For example, a router implementation may require N implementations of a Port module, each differing slightly according to system parameters, but instantiated statically at translation time, not at run-time.
One early approach to these problems was to add a so-called macro preprocessor that (pre)processes the language input and then invokes the language translator itself on the resulting transformed input. This macro processor allows constructs to be specified that are recognized by the macroprocessor, causing it to modify the input source as a textual transformation. For example the C/C++ preprocessor is such a macroprocessor, providing named constants, macros (inline procedures), conditional compilation and file inclusion. However, this approach has several problems in practice. In particular, textual transformations can produce illegal results and difficult error messages as well as introduce more subtle errors. It can also make software debugging difficult because the input source does not reflect exactly what is provided to the language translator proper.
More recently, type-parameterized types and procedures have been included in the language, so common code differing only in the types or constants used can be specified once, and then instantiated for each specific type. The C++ template mechanism is an example. However, a C++ typename has no semantic properties. It is just a type parameter that binds to a class. In particular, instantiation of C++ types can lead to errors that are only discovered at link time, not by the translator. C# and Java generics are similar except they allow for specification of some constraints or invariants for these parameters.
Another approach is to support redefinition of basic language constructs within the language, which is often referred to as meta-programming. For example, the language can allow a programmer to specify a revised definition of basic operators such as the function call operator. However, this approach effectively creates a new language, making it difficult for other programmers to understand the software. It also does not address key practical requirements such as conditional compilation, file inclusion, etc.
Accordingly, it would be an advance in the art to provide mechanisms for handling logical commonality at translation time that provide an improved combination of generality and ease of use relative to conventional approaches.